{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "09f7126f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c6ab2b72",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "import seaborn as sns\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a08d6044",
   "metadata": {},
   "source": [
    "## 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df055add",
   "metadata": {},
   "source": [
    "## 通用导入函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "74bcbf7b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "993c86ce",
   "metadata": {},
   "source": [
    "## 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "722d1e40",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 TRAIN_ASSET_data 已加载为 DataFrame\n",
      "数据集 TRAIN_CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_CUST_INFO_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_TRNFLW_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_NATURE_data 已加载为 DataFrame\n",
      "数据集 TRAIN_PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 TRAIN_TR_APS_DTL_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "train_load_dt = './data/Train'\n",
    "train_data_name = load_data_from_directory(train_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e70968",
   "metadata": {},
   "source": [
    "## 测试集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c3ef0f15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 A_ASSET_data 已加载为 DataFrame\n",
      "数据集 A_CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_CUST_INFO_data 已加载为 DataFrame\n",
      "数据集 A_MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_TRNFLW_DTL_data 已加载为 DataFrame\n",
      "数据集 A_PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 A_TEST_NATURE_data 已加载为 DataFrame\n",
      "数据集 A_TR_APS_DTL_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "A_load_dt = './data/A'\n",
    "A_data_name = load_data_from_directory(A_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6a9129f",
   "metadata": {},
   "source": [
    "# 特征工程"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50ea17ff",
   "metadata": {},
   "source": [
    "## 贷记卡交易明细表特征工程\n",
    "\n",
    "贷记卡交易明细表(CCD_TR_DTL)包含近三个月的交易流水信息,是识别信用卡潜力客户的重要数据源。\n",
    "\n",
    "主要特征维度:\n",
    "1. 基础统计特征:交易笔数、金额统计等\n",
    "2. 时间序列特征:交易频率、规律性、趋势等\n",
    "3. 交易行为特征:消费偏好、渠道偏好、商户偏好等\n",
    "4. 滑窗统计特征:按月/周/日的滑窗统计\n",
    "5. 交叉组合特征:多维度交叉分析"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e42e85ca",
   "metadata": {},
   "source": [
    "### 通用函数定义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "83f45791",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "通用函数定义完成\n"
     ]
    }
   ],
   "source": [
    "def get_days_to_now(df, date_col):\n",
    "    \"\"\"\n",
    "    将日期列转换为距今天数特征\n",
    "    \n",
    "    参数:\n",
    "    - df: 数据框\n",
    "    - date_col: 日期列名\n",
    "    \n",
    "    返回:\n",
    "    - 添加了时间特征的数据框\n",
    "    \"\"\"\n",
    "    df[\"date\"] = pd.to_datetime(df[date_col], format=\"%Y%m%d\")\n",
    "    max_date = df[\"date\"].max()\n",
    "    df_days_to_now = (max_date - df[\"date\"]).dt.days\n",
    "    \n",
    "    df[\"date_months_to_now\"] = df_days_to_now // 31\n",
    "    df[\"date_weeks_to_now\"] = df_days_to_now // 7\n",
    "    df[\"date_days_to_now\"] = df_days_to_now\n",
    "    \n",
    "    return df\n",
    "\n",
    "def get_dense_features(df, col, stat):\n",
    "    \"\"\"按客户ID聚合数值特征\"\"\"\n",
    "    if stat == \"kurt\":\n",
    "        f_stat = lambda x: x.kurt()\n",
    "    elif stat == \"quantile_1_4\":\n",
    "        f_stat = lambda x: x.quantile(0.25)\n",
    "    elif stat == \"quantile_1_2\":\n",
    "        f_stat = lambda x: x.quantile(0.5)\n",
    "    elif stat == \"quantile_3_4\":\n",
    "        f_stat = lambda x: x.quantile(0.75)\n",
    "    else:\n",
    "        f_stat = stat\n",
    "    \n",
    "    group_df = df.groupby(['CUST_NO'])[col].agg(f_stat).reset_index()\n",
    "    group_df.columns = ['CUST_NO', 'CUST_NO_'+'{}_'.format(col)+stat]\n",
    "    return group_df\n",
    "\n",
    "def get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat):\n",
    "    \"\"\"\n",
    "    按客户ID和类别特征聚合\n",
    "    \"\"\"\n",
    "    tmp = df_to_groupby.groupby(['CUST_NO', fea1])[fea2].agg(\n",
    "        stat if stat != \"kurt\" else lambda x: x.kurt()\n",
    "    ).to_frame('_'.join(['CUST_NO', fea1, fea2, stat])).reset_index()\n",
    "    \n",
    "    df_tmp = pd.pivot(data=tmp, index='CUST_NO', columns=fea1, \n",
    "                      values='_'.join(['CUST_NO', fea1, fea2, stat]))\n",
    "    new_fea_cols = ['_'.join(['CUST_NO', fea1, fea2, stat, str(col)]) for col in df_tmp.columns]\n",
    "    df_tmp.columns = new_fea_cols\n",
    "    df_tmp.reset_index(inplace=True)\n",
    "    \n",
    "    if stat == 'count':\n",
    "        df_tmp = df_tmp.fillna(0)\n",
    "    \n",
    "    valid_cols = []\n",
    "    for col in df_tmp.columns:\n",
    "        if not df_tmp[col].isna().all():\n",
    "            valid_cols.append(col)\n",
    "    \n",
    "    df_fea = df_fea.merge(df_tmp[valid_cols], on='CUST_NO', how='left')\n",
    "    return df_fea, new_fea_cols\n",
    "\n",
    "def get_all_id_category_features(df_fea, df_to_groupby, fea1, fea2, stats):\n",
    "    \"\"\"批量生成类别特征交叉统计\"\"\"\n",
    "    all_new_fea_cols = []\n",
    "    for stat in tqdm(stats, desc=f\"生成{fea1}分组特征\"):\n",
    "        df_fea, new_fea_cols = get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat)\n",
    "        all_new_fea_cols += new_fea_cols\n",
    "    return df_fea, all_new_fea_cols\n",
    "\n",
    "def get_division_features(df1, df2, col1, col2, eps=1e-6):\n",
    "    \"\"\"生成除法特征(比值特征)\"\"\"\n",
    "    tmp = pd.merge(df1, df2, how=\"left\", on=\"CUST_NO\")\n",
    "    new_feature_name = '_'.join([col1, \"div\", col2])\n",
    "    tmp[new_feature_name] = tmp[col1] / (tmp[col2] + eps)\n",
    "    feature_name = [\"CUST_NO\", new_feature_name]\n",
    "    return tmp[feature_name]\n",
    "\n",
    "print(\"通用函数定义完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6c8b4aa",
   "metadata": {},
   "source": [
    "### 数据加载与预处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "7c2aa5fd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据预处理函数定义完成\n"
     ]
    }
   ],
   "source": [
    "def preprocess_ccd_data(df, prefix=''):\n",
    "    \"\"\"\n",
    "    预处理贷记卡交易数据\n",
    "    \"\"\"\n",
    "    print(f\"\\n{'='*50}\")\n",
    "    print(f\"开始预处理{prefix}贷记卡交易数据\")\n",
    "    print(f\"{'='*50}\")\n",
    "    \n",
    "    df = df.copy()\n",
    "    \n",
    "    print(f\"原始数据形状: {df.shape}\")\n",
    "    print(f\"客户数量: {df['CUST_NO'].nunique()}\")\n",
    "    print(f\"交易记录数: {len(df)}\")\n",
    "    print(f\"日期范围: {df['TR_DAT'].min()} ~ {df['TR_DAT'].max()}\")\n",
    "    \n",
    "    df = get_days_to_now(df, 'TR_DAT')\n",
    "    \n",
    "    print(f\"距今天数范围: {df['date_days_to_now'].min()} ~ {df['date_days_to_now'].max()}\")\n",
    "    print(f\"月份分布:\\n{df['date_months_to_now'].value_counts().sort_index()}\")\n",
    "    \n",
    "    df['year'] = df['date'].dt.year\n",
    "    df['month'] = df['date'].dt.month\n",
    "    df['day'] = df['date'].dt.day\n",
    "    df['dayofweek'] = df['date'].dt.dayofweek\n",
    "    df['is_weekend'] = (df['dayofweek'] >= 5).astype(int)\n",
    "    df['hour'] = pd.to_datetime(df['TR_TIME'], format='%H%M%S', errors='coerce').dt.hour\n",
    "    \n",
    "    df['TR_AMT'] = pd.to_numeric(df['TR_AMT'], errors='coerce').fillna(0)\n",
    "    \n",
    "    print(f\"\\n交易金额统计:\")\n",
    "    print(df['TR_AMT'].describe())\n",
    "    print(f\"\\n交易方向分布:\")\n",
    "    print(df['TR_DRCT'].value_counts())\n",
    "    \n",
    "    if 'TR_COD' in df.columns:\n",
    "        print(f\"\\n交易码数量: {df['TR_COD'].nunique()}\")\n",
    "    if 'TR_CHANL_CD' in df.columns:\n",
    "        print(f\"交易渠道数量: {df['TR_CHANL_CD'].nunique()}\")\n",
    "    if 'MCHT_NO' in df.columns:\n",
    "        print(f\"商户数量: {df['MCHT_NO'].nunique()}\")\n",
    "    \n",
    "    print(f\"\\n预处理完成!\")\n",
    "    \n",
    "    return df\n",
    "\n",
    "print(\"数据预处理函数定义完成\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "e3c2f624",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "开始预处理训练集贷记卡交易数据\n",
      "==================================================\n",
      "原始数据形状: (1205, 8)\n",
      "客户数量: 169\n",
      "交易记录数: 1205\n",
      "日期范围: 20250201 ~ 20250529\n",
      "距今天数范围: 0 ~ 117\n",
      "月份分布:\n",
      "date_months_to_now\n",
      "0    263\n",
      "1    459\n",
      "2    322\n",
      "3    161\n",
      "Name: count, dtype: int64\n",
      "\n",
      "交易金额统计:\n",
      "count      1205.000000\n",
      "mean      17321.093386\n",
      "std       56884.596115\n",
      "min           0.100000\n",
      "25%         314.570000\n",
      "50%        2033.670000\n",
      "75%        3750.000000\n",
      "max      800000.000000\n",
      "Name: TR_AMT, dtype: float64\n",
      "\n",
      "交易方向分布:\n",
      "TR_DRCT\n",
      " 1    697\n",
      "-1    506\n",
      " 0      2\n",
      "Name: count, dtype: int64\n",
      "\n",
      "交易码数量: 23\n",
      "交易渠道数量: 11\n",
      "商户数量: 54\n",
      "\n",
      "预处理完成!\n",
      "\n",
      "==================================================\n",
      "开始预处理测试集A贷记卡交易数据\n",
      "==================================================\n",
      "原始数据形状: (129, 8)\n",
      "客户数量: 18\n",
      "交易记录数: 129\n",
      "日期范围: 20250401 ~ 20250627\n",
      "距今天数范围: 0 ~ 87\n",
      "月份分布:\n",
      "date_months_to_now\n",
      "0    56\n",
      "1    38\n",
      "2    35\n",
      "Name: count, dtype: int64\n",
      "\n",
      "交易金额统计:\n",
      "count       129.000000\n",
      "mean      12356.753488\n",
      "std       56051.770012\n",
      "min           0.500000\n",
      "25%         333.330000\n",
      "50%        2083.670000\n",
      "75%        3992.000000\n",
      "max      540000.000000\n",
      "Name: TR_AMT, dtype: float64\n",
      "\n",
      "交易方向分布:\n",
      "TR_DRCT\n",
      " 1    71\n",
      "-1    58\n",
      "Name: count, dtype: int64\n",
      "\n",
      "交易码数量: 11\n",
      "交易渠道数量: 5\n",
      "商户数量: 10\n",
      "\n",
      "预处理完成!\n"
     ]
    }
   ],
   "source": [
    "TRAIN_ccd = preprocess_ccd_data(TRAIN_CCD_TR_DTL_data, prefix='训练集')\n",
    "A_ccd = preprocess_ccd_data(A_CCD_TR_DTL_data, prefix='测试集A')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e9e48e8",
   "metadata": {},
   "source": [
    "### 贷记卡特征工程主函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "bf978653",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_basic_features(df):\n",
    "    \"\"\"\n",
    "    生成贷记卡基础统计特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[1/10] 生成基础统计特征...\")\n",
    "    \n",
    "    features = df.groupby('CUST_NO').agg(\n",
    "        ccd_tr_count=('TR_AMT', 'count'),\n",
    "        ccd_amt_sum=('TR_AMT', 'sum'),\n",
    "        ccd_amt_mean=('TR_AMT', 'mean'),\n",
    "        ccd_amt_std=('TR_AMT', 'std'),\n",
    "        ccd_amt_median=('TR_AMT', 'median'),\n",
    "        ccd_amt_max=('TR_AMT', 'max'),\n",
    "        ccd_amt_min=('TR_AMT', 'min'),\n",
    "        ccd_amt_skew=('TR_AMT', lambda x: x.skew()),\n",
    "        ccd_amt_kurt=('TR_AMT', lambda x: x.kurt()),\n",
    "        ccd_amt_q25=('TR_AMT', lambda x: x.quantile(0.25)),\n",
    "        ccd_amt_q75=('TR_AMT', lambda x: x.quantile(0.75)),\n",
    "        \n",
    "        ccd_active_days=('date', lambda x: x.nunique()),\n",
    "        ccd_active_weeks=('date_weeks_to_now', 'nunique'),\n",
    "        ccd_active_months=('date_months_to_now', 'nunique'),\n",
    "        \n",
    "        ccd_weekday_count=('is_weekend', lambda x: (x == 0).sum()),\n",
    "        ccd_weekend_count=('is_weekend', lambda x: (x == 1).sum()),\n",
    "        \n",
    "        ccd_tr_cod_nunique=('TR_COD', 'nunique'),\n",
    "        ccd_tr_chanl_nunique=('TR_CHANL_CD', 'nunique'),\n",
    "        ccd_mcht_nunique=('MCHT_NO', 'nunique'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features['ccd_amt_range'] = features['ccd_amt_max'] - features['ccd_amt_min']\n",
    "    features['ccd_amt_cv'] = features['ccd_amt_std'] / (features['ccd_amt_mean'] + 1e-6)\n",
    "    features['ccd_amt_iqr'] = features['ccd_amt_q75'] - features['ccd_amt_q25']\n",
    "    features['ccd_amt_per_trans'] = features['ccd_amt_sum'] / (features['ccd_tr_count'] + 1e-6)\n",
    "    features['ccd_trans_per_day'] = features['ccd_tr_count'] / (features['ccd_active_days'] + 1e-6)\n",
    "    features['ccd_amt_per_day'] = features['ccd_amt_sum'] / (features['ccd_active_days'] + 1e-6)\n",
    "    features['ccd_weekend_ratio'] = features['ccd_weekend_count'] / (features['ccd_tr_count'] + 1e-6)\n",
    "    features['ccd_weekday_ratio'] = features['ccd_weekday_count'] / (features['ccd_tr_count'] + 1e-6)\n",
    "    \n",
    "    print(f\"基础统计特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "78bffcee",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_time_features(df):\n",
    "    \"\"\"\n",
    "    生成时间序列特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[2/10] 生成时间序列特征...\")\n",
    "    \n",
    "    df_time = df.sort_values(['CUST_NO', 'date']).copy()\n",
    "    df_time['tr_interval'] = df_time.groupby('CUST_NO')['date'].diff().dt.days\n",
    "    \n",
    "    features = df_time.groupby('CUST_NO').agg(\n",
    "        ccd_last_tr_days_ago=('date_days_to_now', 'min'),\n",
    "        ccd_first_tr_days_ago=('date_days_to_now', 'max'),\n",
    "        \n",
    "        ccd_tr_interval_mean=('tr_interval', 'mean'),\n",
    "        ccd_tr_interval_std=('tr_interval', 'std'),\n",
    "        ccd_tr_interval_max=('tr_interval', 'max'),\n",
    "        ccd_tr_interval_min=('tr_interval', 'min'),\n",
    "        ccd_tr_interval_median=('tr_interval', 'median'),\n",
    "        ccd_tr_interval_q25=('tr_interval', lambda x: x.quantile(0.25)),\n",
    "        ccd_tr_interval_q75=('tr_interval', lambda x: x.quantile(0.75)),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features['ccd_tr_interval_cv'] = features['ccd_tr_interval_std'] / (features['ccd_tr_interval_mean'] + 1e-6)\n",
    "    features['ccd_tr_duration_days'] = features['ccd_first_tr_days_ago'] - features['ccd_last_tr_days_ago']\n",
    "    features['ccd_tr_interval_range'] = features['ccd_tr_interval_max'] - features['ccd_tr_interval_min']\n",
    "    \n",
    "    print(f\"时间序列特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "2297b56a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_direction_features(df):\n",
    "    \"\"\"\n",
    "    生成交易方向特征(收入/支出)\n",
    "    \"\"\"\n",
    "    print(\"\\n[3/10] 生成交易方向特征...\")\n",
    "    \n",
    "    features_list = []\n",
    "    \n",
    "    for direction, direction_name in [('1', 'debit'), ('2', 'credit')]:\n",
    "        df_dir = df[df['TR_DRCT'] == direction]\n",
    "        \n",
    "        if len(df_dir) == 0:\n",
    "            continue\n",
    "        \n",
    "        fea = df_dir.groupby('CUST_NO').agg(\n",
    "            count=('TR_AMT', 'count'),\n",
    "            amt_sum=('TR_AMT', 'sum'),\n",
    "            amt_mean=('TR_AMT', 'mean'),\n",
    "            amt_std=('TR_AMT', 'std'),\n",
    "            amt_max=('TR_AMT', 'max'),\n",
    "            amt_min=('TR_AMT', 'min'),\n",
    "            amt_median=('TR_AMT', 'median'),\n",
    "        ).reset_index()\n",
    "        \n",
    "        fea.columns = ['CUST_NO'] + [f'ccd_{direction_name}_{col}' for col in fea.columns if col != 'CUST_NO']\n",
    "        features_list.append(fea)\n",
    "    \n",
    "    if len(features_list) == 0:\n",
    "        print(\"无交易方向数据，返回空特征\")\n",
    "        return pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    features = features_list[0]\n",
    "    for fea in features_list[1:]:\n",
    "        features = features.merge(fea, on='CUST_NO', how='outer')\n",
    "    \n",
    "    if 'ccd_debit_count' in features.columns and 'ccd_credit_count' in features.columns:\n",
    "        features['ccd_debit_ratio'] = features['ccd_debit_count'] / (features['ccd_debit_count'] + features['ccd_credit_count'] + 1e-6)\n",
    "        features['ccd_credit_ratio'] = features['ccd_credit_count'] / (features['ccd_debit_count'] + features['ccd_credit_count'] + 1e-6)\n",
    "    \n",
    "    if 'ccd_debit_amt_sum' in features.columns and 'ccd_credit_amt_sum' in features.columns:\n",
    "        features['ccd_debit_amt_ratio'] = features['ccd_debit_amt_sum'] / (features['ccd_debit_amt_sum'] + features['ccd_credit_amt_sum'] + 1e-6)\n",
    "        features['ccd_credit_amt_ratio'] = features['ccd_credit_amt_sum'] / (features['ccd_debit_amt_sum'] + features['ccd_credit_amt_sum'] + 1e-6)\n",
    "        features['ccd_net_amt'] = features['ccd_credit_amt_sum'] - features['ccd_debit_amt_sum']\n",
    "    \n",
    "    print(f\"交易方向特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "9a72a6ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_amount_dist_features(df):\n",
    "    \"\"\"\n",
    "    生成金额分布特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[4/10] 生成金额分布特征...\")\n",
    "    \n",
    "    def amount_distribution(group):\n",
    "        amt = group['TR_AMT']\n",
    "        mean_amt = amt.mean()\n",
    "        std_amt = amt.std()\n",
    "        \n",
    "        large_threshold = mean_amt + 2 * std_amt\n",
    "        large_count = (amt > large_threshold).sum()\n",
    "        large_ratio = large_count / len(amt) if len(amt) > 0 else 0\n",
    "        large_amt_sum = amt[amt > large_threshold].sum()\n",
    "        \n",
    "        small_threshold = mean_amt\n",
    "        small_count = (amt < small_threshold).sum()\n",
    "        small_ratio = small_count / len(amt) if len(amt) > 0 else 0\n",
    "        \n",
    "        top3_sum = amt.nlargest(min(3, len(amt))).sum()\n",
    "        top3_ratio = top3_sum / amt.sum() if amt.sum() > 0 else 0\n",
    "        \n",
    "        top5_sum = amt.nlargest(min(5, len(amt))).sum()\n",
    "        top5_ratio = top5_sum / amt.sum() if amt.sum() > 0 else 0\n",
    "        \n",
    "        top10_sum = amt.nlargest(min(10, len(amt))).sum()\n",
    "        top10_ratio = top10_sum / amt.sum() if amt.sum() > 0 else 0\n",
    "        \n",
    "        return pd.Series({\n",
    "            'ccd_large_trans_count': large_count,\n",
    "            'ccd_large_trans_ratio': large_ratio,\n",
    "            'ccd_large_amt_sum': large_amt_sum,\n",
    "            'ccd_small_trans_count': small_count,\n",
    "            'ccd_small_trans_ratio': small_ratio,\n",
    "            'ccd_top3_amt_ratio': top3_ratio,\n",
    "            'ccd_top5_amt_ratio': top5_ratio,\n",
    "            'ccd_top10_amt_ratio': top10_ratio,\n",
    "        })\n",
    "    \n",
    "    features = df.groupby('CUST_NO').apply(amount_distribution).reset_index()\n",
    "    \n",
    "    print(f\"金额分布特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "58b2d718",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_month_features(df):\n",
    "    \"\"\"\n",
    "    生成月度特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[5/10] 生成月度特征...\")\n",
    "    \n",
    "    df_month = df.groupby(['CUST_NO', 'date_months_to_now']).agg(\n",
    "        month_count=('TR_AMT', 'count'),\n",
    "        month_amt_sum=('TR_AMT', 'sum'),\n",
    "        month_amt_mean=('TR_AMT', 'mean'),\n",
    "        month_amt_std=('TR_AMT', 'std'),\n",
    "        month_amt_max=('TR_AMT', 'max'),\n",
    "        month_days=('date', 'nunique'),\n",
    "        month_tr_cod_nunique=('TR_COD', 'nunique'),\n",
    "        month_tr_chanl_nunique=('TR_CHANL_CD', 'nunique'),\n",
    "        month_mcht_nunique=('MCHT_NO', 'nunique'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features = df_month.groupby('CUST_NO').agg(\n",
    "        ccd_active_months=('date_months_to_now', 'nunique'),\n",
    "        \n",
    "        ccd_avg_month_count=('month_count', 'mean'),\n",
    "        ccd_month_count_std=('month_count', 'std'),\n",
    "        ccd_month_count_max=('month_count', 'max'),\n",
    "        ccd_month_count_min=('month_count', 'min'),\n",
    "        \n",
    "        ccd_avg_month_amt=('month_amt_sum', 'mean'),\n",
    "        ccd_month_amt_std=('month_amt_sum', 'std'),\n",
    "        ccd_month_amt_max=('month_amt_sum', 'max'),\n",
    "        ccd_month_amt_min=('month_amt_sum', 'min'),\n",
    "        \n",
    "        ccd_avg_month_days=('month_days', 'mean'),\n",
    "        ccd_month_days_std=('month_days', 'std'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features['ccd_month_count_cv'] = features['ccd_month_count_std'] / (features['ccd_avg_month_count'] + 1e-6)\n",
    "    features['ccd_month_amt_cv'] = features['ccd_month_amt_std'] / (features['ccd_avg_month_amt'] + 1e-6)\n",
    "    features['ccd_month_count_range'] = features['ccd_month_count_max'] - features['ccd_month_count_min']\n",
    "    features['ccd_month_amt_range'] = features['ccd_month_amt_max'] - features['ccd_month_amt_min']\n",
    "    \n",
    "    month_pivot = df_month.pivot_table(index='CUST_NO', columns='date_months_to_now', \n",
    "                                       values='month_amt_sum', aggfunc='sum')\n",
    "    \n",
    "    for month in [0, 1, 2]:\n",
    "        if month in month_pivot.columns:\n",
    "            month_pivot[f'ccd_month{month}_amt'] = month_pivot[month]\n",
    "        else:\n",
    "            month_pivot[f'ccd_month{month}_amt'] = 0\n",
    "    \n",
    "    if 0 in month_pivot.columns and 1 in month_pivot.columns:\n",
    "        month_pivot['ccd_amt_growth_m0_m1'] = (month_pivot[0] - month_pivot[1]) / (month_pivot[1] + 1e-6)\n",
    "    if 1 in month_pivot.columns and 2 in month_pivot.columns:\n",
    "        month_pivot['ccd_amt_growth_m1_m2'] = (month_pivot[1] - month_pivot[2]) / (month_pivot[2] + 1e-6)\n",
    "    if 0 in month_pivot.columns and 2 in month_pivot.columns:\n",
    "        month_pivot['ccd_amt_growth_m0_m2'] = (month_pivot[0] - month_pivot[2]) / (month_pivot[2] + 1e-6)\n",
    "    \n",
    "    month_pivot = month_pivot[[col for col in month_pivot.columns if isinstance(col, str) and col.startswith('ccd_')]].reset_index()\n",
    "    features = features.merge(month_pivot, on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"月度特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "bf222332",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_hour_features(df):\n",
    "    \"\"\"\n",
    "    生成小时维度特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[6/10] 生成小时维度特征...\")\n",
    "    \n",
    "    df_valid_hour = df[df['hour'].notna()].copy()\n",
    "    \n",
    "    if len(df_valid_hour) == 0:\n",
    "        print(\"无有效小时数据，跳过\")\n",
    "        return pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    df_valid_hour['time_period'] = df_valid_hour['hour'].apply(lambda x: \n",
    "        'night' if x < 6 else ('morning' if x < 12 else ('afternoon' if x < 18 else 'evening'))\n",
    "    )\n",
    "    \n",
    "    features = df_valid_hour.groupby('CUST_NO').agg(\n",
    "        ccd_hour_mean=('hour', 'mean'),\n",
    "        ccd_hour_std=('hour', 'std'),\n",
    "        ccd_hour_nunique=('hour', 'nunique'),\n",
    "        \n",
    "        ccd_night_count=('time_period', lambda x: (x == 'night').sum()),\n",
    "        ccd_morning_count=('time_period', lambda x: (x == 'morning').sum()),\n",
    "        ccd_afternoon_count=('time_period', lambda x: (x == 'afternoon').sum()),\n",
    "        ccd_evening_count=('time_period', lambda x: (x == 'evening').sum()),\n",
    "    ).reset_index()\n",
    "    \n",
    "    total_count = features[['ccd_night_count', 'ccd_morning_count', 'ccd_afternoon_count', 'ccd_evening_count']].sum(axis=1)\n",
    "    features['ccd_night_ratio'] = features['ccd_night_count'] / (total_count + 1e-6)\n",
    "    features['ccd_morning_ratio'] = features['ccd_morning_count'] / (total_count + 1e-6)\n",
    "    features['ccd_afternoon_ratio'] = features['ccd_afternoon_count'] / (total_count + 1e-6)\n",
    "    features['ccd_evening_ratio'] = features['ccd_evening_count'] / (total_count + 1e-6)\n",
    "    \n",
    "    print(f\"小时维度特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "e2e0cb4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_channel_features(df):\n",
    "    \"\"\"\n",
    "    生成渠道特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[7/10] 生成渠道特征...\")\n",
    "    \n",
    "    def channel_concentration(x):\n",
    "        x = x.dropna()\n",
    "        if len(x) == 0:\n",
    "            return 0\n",
    "        vc = x.value_counts()\n",
    "        if len(vc) == 0:\n",
    "            return 0\n",
    "        return vc.iloc[0] / len(x)\n",
    "    \n",
    "    features = df.groupby('CUST_NO').agg(\n",
    "        ccd_main_chanl_ratio=('TR_CHANL_CD', channel_concentration),\n",
    "        ccd_chanl_diversity=('TR_CHANL_CD', 'nunique'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    chanl_count = df.groupby(['CUST_NO', 'TR_CHANL_CD']).size().reset_index(name='count')\n",
    "    chanl_pivot = chanl_count.pivot_table(index='CUST_NO', columns='TR_CHANL_CD', \n",
    "                                          values='count', fill_value=0)\n",
    "    chanl_pivot.columns = [f'ccd_chanl_{col}_count' for col in chanl_pivot.columns]\n",
    "    chanl_pivot = chanl_pivot.reset_index()\n",
    "    \n",
    "    total_count = df.groupby('CUST_NO').size().reset_index(name='total_count')\n",
    "    chanl_pivot = chanl_pivot.merge(total_count, on='CUST_NO', how='left')\n",
    "    \n",
    "    for col in chanl_pivot.columns:\n",
    "        if col.startswith('ccd_chanl_') and col.endswith('_count'):\n",
    "            ratio_col = col.replace('_count', '_ratio')\n",
    "            chanl_pivot[ratio_col] = chanl_pivot[col] / (chanl_pivot['total_count'] + 1e-6)\n",
    "    \n",
    "    chanl_pivot = chanl_pivot.drop('total_count', axis=1)\n",
    "    \n",
    "    features = features.merge(chanl_pivot, on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"渠道特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "258fdae8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_merchant_features(df):\n",
    "    \"\"\"\n",
    "    生成商户特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[8/10] 生成商户特征...\")\n",
    "    \n",
    "    def merchant_concentration(x):\n",
    "        x = x.dropna()\n",
    "        if len(x) == 0:\n",
    "            return 0\n",
    "        vc = x.value_counts()\n",
    "        if len(vc) == 0:\n",
    "            return 0\n",
    "        return vc.iloc[0] / len(x)\n",
    "    \n",
    "    features = df.groupby('CUST_NO').agg(\n",
    "        ccd_mcht_count=('MCHT_NO', 'nunique'),\n",
    "        ccd_main_mcht_ratio=('MCHT_NO', merchant_concentration),\n",
    "    ).reset_index()\n",
    "    \n",
    "    mcht_amt = df.groupby(['CUST_NO', 'MCHT_NO'])['TR_AMT'].agg(['sum', 'mean', 'count']).reset_index()\n",
    "    \n",
    "    max_mcht_amt = mcht_amt.groupby('CUST_NO').apply(\n",
    "        lambda x: x.loc[x['sum'].idxmax(), 'sum'] / x['sum'].sum() if len(x) > 0 and x['sum'].sum() > 0 else 0\n",
    "    ).reset_index(name='ccd_max_mcht_amt_ratio')\n",
    "    \n",
    "    features = features.merge(max_mcht_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    mcht_top = mcht_amt.sort_values(['CUST_NO', 'sum'], ascending=[True, False])\n",
    "    mcht_top3 = mcht_top.groupby('CUST_NO').head(3).groupby('CUST_NO')['sum'].sum().reset_index(name='top3_sum')\n",
    "    total_amt = df.groupby('CUST_NO')['TR_AMT'].sum().reset_index(name='total_sum')\n",
    "    mcht_top3 = mcht_top3.merge(total_amt, on='CUST_NO', how='left')\n",
    "    mcht_top3['ccd_top3_mcht_amt_ratio'] = mcht_top3['top3_sum'] / (mcht_top3['total_sum'] + 1e-6)\n",
    "    \n",
    "    features = features.merge(mcht_top3[['CUST_NO', 'ccd_top3_mcht_amt_ratio']], on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"商户特征: {len(features.columns)-1}个\")\n",
    "    return features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "729ab4d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_sliding_window_features(df):\n",
    "    \"\"\"\n",
    "    生成滑窗统计特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[9/10] 生成滑窗统计特征...\")\n",
    "    \n",
    "    df_by_day = df.groupby(['CUST_NO', 'date_days_to_now', 'date_weeks_to_now', 'date_months_to_now']).agg({\n",
    "        'TR_AMT': 'sum'\n",
    "    }).reset_index()\n",
    "    \n",
    "    base_features = df[['CUST_NO']].drop_duplicates().reset_index(drop=True)\n",
    "    \n",
    "    for fea1 in ['date_months_to_now']:\n",
    "        tmp_features, _ = get_all_id_category_features(\n",
    "            base_features, df_by_day, fea1=fea1, fea2='TR_AMT',\n",
    "            stats=['mean', 'max', 'min', 'median', 'std', 'sum']\n",
    "        )\n",
    "        base_features = tmp_features\n",
    "    \n",
    "    df_by_day_count = df.groupby(['CUST_NO', 'date_months_to_now']).size().reset_index(name='count')\n",
    "    \n",
    "    for fea1 in ['date_months_to_now']:\n",
    "        tmp_features, _ = get_all_id_category_features(\n",
    "            base_features, df_by_day_count, fea1=fea1, fea2='count',\n",
    "            stats=['sum']\n",
    "        )\n",
    "        base_features = tmp_features\n",
    "    \n",
    "    col_rename = {col: f'ccd_sliding_{col}' for col in base_features.columns if col != 'CUST_NO'}\n",
    "    base_features = base_features.rename(columns=col_rename)\n",
    "    \n",
    "    print(f\"滑窗统计特征: {len(base_features.columns)-1}个\")\n",
    "    return base_features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "06b37b6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_ccd_behavior_features(df):\n",
    "    \"\"\"\n",
    "    生成消费行为特征\n",
    "    \"\"\"\n",
    "    print(\"\\n[10/10] 生成消费行为特征...\")\n",
    "    \n",
    "    features = []\n",
    "    \n",
    "    df_sorted = df.sort_values(['CUST_NO', 'date']).copy()\n",
    "    \n",
    "    df_sorted['amt_change'] = df_sorted.groupby('CUST_NO')['TR_AMT'].diff()\n",
    "    df_sorted['amt_change_pct'] = df_sorted.groupby('CUST_NO')['TR_AMT'].pct_change()\n",
    "    \n",
    "    behavior_features = df_sorted.groupby('CUST_NO').agg(\n",
    "        ccd_amt_change_mean=('amt_change', 'mean'),\n",
    "        ccd_amt_change_std=('amt_change', 'std'),\n",
    "        ccd_amt_change_pct_mean=('amt_change_pct', 'mean'),\n",
    "        ccd_amt_change_pct_std=('amt_change_pct', 'std'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features.append(behavior_features)\n",
    "    \n",
    "    df_sorted['is_same_chanl'] = (df_sorted.groupby('CUST_NO')['TR_CHANL_CD'].shift(1) == df_sorted['TR_CHANL_CD']).astype(int)\n",
    "    df_sorted['is_same_mcht'] = (df_sorted.groupby('CUST_NO')['MCHT_NO'].shift(1) == df_sorted['MCHT_NO']).astype(int)\n",
    "    \n",
    "    continuity_features = df_sorted.groupby('CUST_NO').agg(\n",
    "        ccd_same_chanl_ratio=('is_same_chanl', 'mean'),\n",
    "        ccd_same_mcht_ratio=('is_same_mcht', 'mean'),\n",
    "    ).reset_index()\n",
    "    \n",
    "    features.append(continuity_features)\n",
    "    \n",
    "    recent_7d = df[df['date_days_to_now'] <= 7]\n",
    "    recent_14d = df[df['date_days_to_now'] <= 14]\n",
    "    recent_30d = df[df['date_days_to_now'] <= 30]\n",
    "    \n",
    "    recent_features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    for period_name, period_df in [('7d', recent_7d), ('14d', recent_14d), ('30d', recent_30d)]:\n",
    "        period_stats = period_df.groupby('CUST_NO').agg(\n",
    "            count=('TR_AMT', 'count'),\n",
    "            amt_sum=('TR_AMT', 'sum'),\n",
    "            amt_mean=('TR_AMT', 'mean'),\n",
    "        ).reset_index()\n",
    "        period_stats.columns = ['CUST_NO'] + [f'ccd_recent_{period_name}_{col}' for col in period_stats.columns if col != 'CUST_NO']\n",
    "        recent_features = recent_features.merge(period_stats, on='CUST_NO', how='left')\n",
    "    \n",
    "    features.append(recent_features)\n",
    "    \n",
    "    final_features = features[0]\n",
    "    for fea in features[1:]:\n",
    "        final_features = final_features.merge(fea, on='CUST_NO', how='left')\n",
    "    \n",
    "    print(f\"消费行为特征: {len(final_features.columns)-1}个\")\n",
    "    return final_features"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2d5960f",
   "metadata": {},
   "source": [
    "### 特征整合函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "072b1174",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "特征整合函数定义完成\n"
     ]
    }
   ],
   "source": [
    "def generate_all_ccd_features(df):\n",
    "    \"\"\"\n",
    "    生成所有贷记卡特征\n",
    "    \"\"\"\n",
    "    print(\"\\n\" + \"=\"*60)\n",
    "    print(\"开始生成贷记卡交易明细表特征\")\n",
    "    print(\"=\"*60)\n",
    "    \n",
    "    feature_list = []\n",
    "    \n",
    "    feature_list.append(generate_ccd_basic_features(df))\n",
    "    feature_list.append(generate_ccd_time_features(df))\n",
    "    feature_list.append(generate_ccd_direction_features(df))\n",
    "    feature_list.append(generate_ccd_amount_dist_features(df))\n",
    "    feature_list.append(generate_ccd_month_features(df))\n",
    "    feature_list.append(generate_ccd_hour_features(df))\n",
    "    feature_list.append(generate_ccd_channel_features(df))\n",
    "    feature_list.append(generate_ccd_merchant_features(df))\n",
    "    feature_list.append(generate_ccd_sliding_window_features(df))\n",
    "    feature_list.append(generate_ccd_behavior_features(df))\n",
    "    \n",
    "    print(\"\\n\" + \"=\"*60)\n",
    "    print(\"合并所有特征...\")\n",
    "    print(\"=\"*60)\n",
    "    \n",
    "    final_features = feature_list[0]\n",
    "    for i, fea in enumerate(feature_list[1:], 1):\n",
    "        print(f\"合并第{i+1}个特征集...\")\n",
    "        final_features = final_features.merge(fea, on='CUST_NO', how='outer')\n",
    "    \n",
    "    print(f\"\\n特征工程完成!\")\n",
    "    print(f\"总客户数: {len(final_features)}\")\n",
    "    print(f\"总特征数: {len(final_features.columns) - 1}\")\n",
    "    print(f\"特征维度: {final_features.shape}\")\n",
    "    \n",
    "    return final_features\n",
    "\n",
    "print(\"特征整合函数定义完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e219a81",
   "metadata": {},
   "source": [
    "### 执行特征工程 - 训练集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "990db0a4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "============================================================\n",
      "开始生成贷记卡交易明细表特征\n",
      "============================================================\n",
      "\n",
      "[1/10] 生成基础统计特征...\n",
      "基础统计特征: 27个\n",
      "\n",
      "[2/10] 生成时间序列特征...\n",
      "时间序列特征: 12个\n",
      "\n",
      "[3/10] 生成交易方向特征...\n",
      "无交易方向数据，返回空特征\n",
      "\n",
      "[4/10] 生成金额分布特征...\n",
      "金额分布特征: 8个\n",
      "\n",
      "[5/10] 生成月度特征...\n",
      "月度特征: 21个\n",
      "\n",
      "[6/10] 生成小时维度特征...\n",
      "小时维度特征: 11个\n",
      "\n",
      "[7/10] 生成渠道特征...\n",
      "渠道特征: 24个\n",
      "\n",
      "[8/10] 生成商户特征...\n",
      "商户特征: 4个\n",
      "\n",
      "[9/10] 生成滑窗统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "生成date_months_to_now分组特征: 100%|██████████| 6/6 [00:00<00:00, 483.10it/s]\n",
      "生成date_months_to_now分组特征: 100%|██████████| 1/1 [00:00<00:00, 333.23it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "滑窗统计特征: 28个\n",
      "\n",
      "[10/10] 生成消费行为特征...\n",
      "消费行为特征: 15个\n",
      "\n",
      "============================================================\n",
      "合并所有特征...\n",
      "============================================================\n",
      "合并第2个特征集...\n",
      "合并第3个特征集...\n",
      "合并第4个特征集...\n",
      "合并第5个特征集...\n",
      "合并第6个特征集...\n",
      "合并第7个特征集...\n",
      "合并第8个特征集...\n",
      "合并第9个特征集...\n",
      "合并第10个特征集...\n",
      "\n",
      "特征工程完成!\n",
      "总客户数: 169\n",
      "总特征数: 150\n",
      "特征维度: (169, 151)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "TRAIN_ccd_features = generate_all_ccd_features(TRAIN_ccd)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "e103f0a2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>ccd_tr_count</th>\n",
       "      <th>ccd_amt_sum</th>\n",
       "      <th>ccd_amt_mean</th>\n",
       "      <th>ccd_amt_std</th>\n",
       "      <th>ccd_amt_median</th>\n",
       "      <th>ccd_amt_max</th>\n",
       "      <th>ccd_amt_min</th>\n",
       "      <th>ccd_amt_skew</th>\n",
       "      <th>ccd_amt_kurt</th>\n",
       "      <th>...</th>\n",
       "      <th>ccd_same_mcht_ratio</th>\n",
       "      <th>ccd_recent_7d_count</th>\n",
       "      <th>ccd_recent_7d_amt_sum</th>\n",
       "      <th>ccd_recent_7d_amt_mean</th>\n",
       "      <th>ccd_recent_14d_count</th>\n",
       "      <th>ccd_recent_14d_amt_sum</th>\n",
       "      <th>ccd_recent_14d_amt_mean</th>\n",
       "      <th>ccd_recent_30d_count</th>\n",
       "      <th>ccd_recent_30d_amt_sum</th>\n",
       "      <th>ccd_recent_30d_amt_mean</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>02d7fd72e665c93b6acd905f8daa404b</td>\n",
       "      <td>6</td>\n",
       "      <td>6009.44</td>\n",
       "      <td>1001.573333</td>\n",
       "      <td>961.266563</td>\n",
       "      <td>976.465</td>\n",
       "      <td>1986.07</td>\n",
       "      <td>122.27</td>\n",
       "      <td>0.015996</td>\n",
       "      <td>-3.285709</td>\n",
       "      <td>...</td>\n",
       "      <td>0.833333</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>045af69eb3aadd04ed897770c1651a33</td>\n",
       "      <td>21</td>\n",
       "      <td>21687.12</td>\n",
       "      <td>1032.720000</td>\n",
       "      <td>711.862589</td>\n",
       "      <td>1051.660</td>\n",
       "      <td>2319.51</td>\n",
       "      <td>122.66</td>\n",
       "      <td>0.326375</td>\n",
       "      <td>-0.315439</td>\n",
       "      <td>...</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0461825b0fca0afedf56a97581d1678c</td>\n",
       "      <td>1</td>\n",
       "      <td>100000.00</td>\n",
       "      <td>100000.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>100000.000</td>\n",
       "      <td>100000.00</td>\n",
       "      <td>100000.00</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.0</td>\n",
       "      <td>100000.0</td>\n",
       "      <td>100000.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>08d19a3313b151b8ee354a3793110fc2</td>\n",
       "      <td>5</td>\n",
       "      <td>106467.68</td>\n",
       "      <td>21293.536000</td>\n",
       "      <td>44004.171708</td>\n",
       "      <td>2033.670</td>\n",
       "      <td>100000.00</td>\n",
       "      <td>366.67</td>\n",
       "      <td>2.234555</td>\n",
       "      <td>4.994665</td>\n",
       "      <td>...</td>\n",
       "      <td>0.400000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>09aaa0c493d63f796a80e81663bf5b58</td>\n",
       "      <td>6</td>\n",
       "      <td>235749.66</td>\n",
       "      <td>39291.610000</td>\n",
       "      <td>78718.707210</td>\n",
       "      <td>2475.000</td>\n",
       "      <td>198000.00</td>\n",
       "      <td>133.33</td>\n",
       "      <td>2.324235</td>\n",
       "      <td>5.467354</td>\n",
       "      <td>...</td>\n",
       "      <td>0.666667</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 151 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  ccd_tr_count  ccd_amt_sum   ccd_amt_mean  \\\n",
       "0  02d7fd72e665c93b6acd905f8daa404b             6      6009.44    1001.573333   \n",
       "1  045af69eb3aadd04ed897770c1651a33            21     21687.12    1032.720000   \n",
       "2  0461825b0fca0afedf56a97581d1678c             1    100000.00  100000.000000   \n",
       "3  08d19a3313b151b8ee354a3793110fc2             5    106467.68   21293.536000   \n",
       "4  09aaa0c493d63f796a80e81663bf5b58             6    235749.66   39291.610000   \n",
       "\n",
       "    ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_skew  \\\n",
       "0    961.266563         976.465      1986.07       122.27      0.015996   \n",
       "1    711.862589        1051.660      2319.51       122.66      0.326375   \n",
       "2           NaN      100000.000    100000.00    100000.00           NaN   \n",
       "3  44004.171708        2033.670    100000.00       366.67      2.234555   \n",
       "4  78718.707210        2475.000    198000.00       133.33      2.324235   \n",
       "\n",
       "   ccd_amt_kurt  ...  ccd_same_mcht_ratio  ccd_recent_7d_count  \\\n",
       "0     -3.285709  ...             0.833333                  NaN   \n",
       "1     -0.315439  ...             0.952381                  NaN   \n",
       "2           NaN  ...             0.000000                  NaN   \n",
       "3      4.994665  ...             0.400000                  NaN   \n",
       "4      5.467354  ...             0.666667                  NaN   \n",
       "\n",
       "   ccd_recent_7d_amt_sum  ccd_recent_7d_amt_mean  ccd_recent_14d_count  \\\n",
       "0                    NaN                     NaN                   NaN   \n",
       "1                    NaN                     NaN                   NaN   \n",
       "2                    NaN                     NaN                   NaN   \n",
       "3                    NaN                     NaN                   NaN   \n",
       "4                    NaN                     NaN                   NaN   \n",
       "\n",
       "   ccd_recent_14d_amt_sum  ccd_recent_14d_amt_mean  ccd_recent_30d_count  \\\n",
       "0                     NaN                      NaN                   NaN   \n",
       "1                     NaN                      NaN                   NaN   \n",
       "2                     NaN                      NaN                   1.0   \n",
       "3                     NaN                      NaN                   NaN   \n",
       "4                     NaN                      NaN                   NaN   \n",
       "\n",
       "   ccd_recent_30d_amt_sum  ccd_recent_30d_amt_mean  \n",
       "0                     NaN                      NaN  \n",
       "1                     NaN                      NaN  \n",
       "2                100000.0                 100000.0  \n",
       "3                     NaN                      NaN  \n",
       "4                     NaN                      NaN  \n",
       "\n",
       "[5 rows x 151 columns]"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "TRAIN_ccd_features.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "8e020eeb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "训练集特征信息:\n",
      "客户数: 169\n",
      "特征数: 150\n",
      "缺失值统计:\n",
      "ccd_recent_7d_amt_mean                                    164\n",
      "ccd_recent_7d_count                                       164\n",
      "ccd_recent_7d_amt_sum                                     164\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_std_3       144\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_max_3       138\n",
      "ccd_sliding_CUST_NO_date_months_to_now_count_sum_3        138\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_sum_3       138\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_mean_3      138\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_median_3    138\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_min_3       138\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "print(f\"训练集特征信息:\")\n",
    "print(f\"客户数: {TRAIN_ccd_features['CUST_NO'].nunique()}\")\n",
    "print(f\"特征数: {len(TRAIN_ccd_features.columns) - 1}\")\n",
    "print(f\"缺失值统计:\")\n",
    "print(TRAIN_ccd_features.isnull().sum().sort_values(ascending=False).head(10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27c816b9",
   "metadata": {},
   "source": [
    "### 保存训练集特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "f92dcb43",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "训练集特征文件已保存: ./feature\\TRAIN_CCD_TR_DTL_features.pkl\n",
      "文件大小: 0.20 MB\n"
     ]
    }
   ],
   "source": [
    "feature_dir = './feature'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "output_file = os.path.join(feature_dir, 'TRAIN_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(TRAIN_ccd_features, f)\n",
    "\n",
    "print(f\"\\n训练集特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7a9fbcc",
   "metadata": {},
   "source": [
    "### 执行特征工程 - 测试集A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "483adadd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "============================================================\n",
      "开始生成贷记卡交易明细表特征\n",
      "============================================================\n",
      "\n",
      "[1/10] 生成基础统计特征...\n",
      "基础统计特征: 27个\n",
      "\n",
      "[2/10] 生成时间序列特征...\n",
      "时间序列特征: 12个\n",
      "\n",
      "[3/10] 生成交易方向特征...\n",
      "无交易方向数据，返回空特征\n",
      "\n",
      "[4/10] 生成金额分布特征...\n",
      "金额分布特征: 8个\n",
      "\n",
      "[5/10] 生成月度特征...\n",
      "月度特征: 21个\n",
      "\n",
      "[6/10] 生成小时维度特征...\n",
      "小时维度特征: 11个\n",
      "\n",
      "[7/10] 生成渠道特征...\n",
      "渠道特征: 12个\n",
      "\n",
      "[8/10] 生成商户特征...\n",
      "商户特征: 4个\n",
      "\n",
      "[9/10] 生成滑窗统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "生成date_months_to_now分组特征: 100%|██████████| 6/6 [00:00<00:00, 400.05it/s]\n",
      "生成date_months_to_now分组特征: 100%|██████████| 1/1 [00:00<00:00, 498.97it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "滑窗统计特征: 21个\n",
      "\n",
      "[10/10] 生成消费行为特征...\n",
      "消费行为特征: 15个\n",
      "\n",
      "============================================================\n",
      "合并所有特征...\n",
      "============================================================\n",
      "合并第2个特征集...\n",
      "合并第3个特征集...\n",
      "合并第4个特征集...\n",
      "合并第5个特征集...\n",
      "合并第6个特征集...\n",
      "合并第7个特征集...\n",
      "合并第8个特征集...\n",
      "合并第9个特征集...\n",
      "合并第10个特征集...\n",
      "\n",
      "特征工程完成!\n",
      "总客户数: 18\n",
      "总特征数: 131\n",
      "特征维度: (18, 132)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "A_ccd_features = generate_all_ccd_features(A_ccd)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "a97de357",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>ccd_tr_count</th>\n",
       "      <th>ccd_amt_sum</th>\n",
       "      <th>ccd_amt_mean</th>\n",
       "      <th>ccd_amt_std</th>\n",
       "      <th>ccd_amt_median</th>\n",
       "      <th>ccd_amt_max</th>\n",
       "      <th>ccd_amt_min</th>\n",
       "      <th>ccd_amt_skew</th>\n",
       "      <th>ccd_amt_kurt</th>\n",
       "      <th>...</th>\n",
       "      <th>ccd_same_mcht_ratio</th>\n",
       "      <th>ccd_recent_7d_count</th>\n",
       "      <th>ccd_recent_7d_amt_sum</th>\n",
       "      <th>ccd_recent_7d_amt_mean</th>\n",
       "      <th>ccd_recent_14d_count</th>\n",
       "      <th>ccd_recent_14d_amt_sum</th>\n",
       "      <th>ccd_recent_14d_amt_mean</th>\n",
       "      <th>ccd_recent_30d_count</th>\n",
       "      <th>ccd_recent_30d_amt_sum</th>\n",
       "      <th>ccd_recent_30d_amt_mean</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>12</td>\n",
       "      <td>27996.96</td>\n",
       "      <td>2333.080000</td>\n",
       "      <td>1055.138950</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>583.33</td>\n",
       "      <td>-1.326650</td>\n",
       "      <td>-0.325926</td>\n",
       "      <td>...</td>\n",
       "      <td>0.916667</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3.0</td>\n",
       "      <td>8748.99</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>4.0</td>\n",
       "      <td>9332.32</td>\n",
       "      <td>2333.080</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>9</td>\n",
       "      <td>3004.00</td>\n",
       "      <td>333.777778</td>\n",
       "      <td>999.833333</td>\n",
       "      <td>0.50</td>\n",
       "      <td>3000.00</td>\n",
       "      <td>0.50</td>\n",
       "      <td>3.000000</td>\n",
       "      <td>9.000000</td>\n",
       "      <td>...</td>\n",
       "      <td>0.888889</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>12</td>\n",
       "      <td>41472.00</td>\n",
       "      <td>3456.000000</td>\n",
       "      <td>1885.049360</td>\n",
       "      <td>4497.00</td>\n",
       "      <td>4500.00</td>\n",
       "      <td>330.00</td>\n",
       "      <td>-1.326648</td>\n",
       "      <td>-0.325928</td>\n",
       "      <td>...</td>\n",
       "      <td>0.916667</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.0</td>\n",
       "      <td>4500.00</td>\n",
       "      <td>4500.00</td>\n",
       "      <td>4.0</td>\n",
       "      <td>13824.00</td>\n",
       "      <td>3456.000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>5</td>\n",
       "      <td>85332.32</td>\n",
       "      <td>17066.464000</td>\n",
       "      <td>35185.650870</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>80000.00</td>\n",
       "      <td>333.33</td>\n",
       "      <td>2.234555</td>\n",
       "      <td>4.994665</td>\n",
       "      <td>...</td>\n",
       "      <td>0.600000</td>\n",
       "      <td>3.0</td>\n",
       "      <td>4998.99</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>3.0</td>\n",
       "      <td>4998.99</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>5.0</td>\n",
       "      <td>85332.32</td>\n",
       "      <td>17066.464</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>10</td>\n",
       "      <td>130.00</td>\n",
       "      <td>13.000000</td>\n",
       "      <td>9.055385</td>\n",
       "      <td>20.00</td>\n",
       "      <td>20.00</td>\n",
       "      <td>1.00</td>\n",
       "      <td>-0.499411</td>\n",
       "      <td>-2.224845</td>\n",
       "      <td>...</td>\n",
       "      <td>0.900000</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.00</td>\n",
       "      <td>1.00</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.00</td>\n",
       "      <td>1.00</td>\n",
       "      <td>4.0</td>\n",
       "      <td>44.00</td>\n",
       "      <td>11.000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 132 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  ccd_tr_count  ccd_amt_sum  ccd_amt_mean  \\\n",
       "0  064898a7d7d2bc34872e423170f97bfb            12     27996.96   2333.080000   \n",
       "1  0b3a60d7823eacb570f33592bca838ad             9      3004.00    333.777778   \n",
       "2  2748b8bdd1889567b1755668e79b5ee7            12     41472.00   3456.000000   \n",
       "3  42c6b970935d3833c31276fe60aba663             5     85332.32  17066.464000   \n",
       "4  4544d13ddc128e03327fe7ffac914765            10       130.00     13.000000   \n",
       "\n",
       "    ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_skew  \\\n",
       "0   1055.138950         2916.33      2916.33       583.33     -1.326650   \n",
       "1    999.833333            0.50      3000.00         0.50      3.000000   \n",
       "2   1885.049360         4497.00      4500.00       330.00     -1.326648   \n",
       "3  35185.650870         1666.33     80000.00       333.33      2.234555   \n",
       "4      9.055385           20.00        20.00         1.00     -0.499411   \n",
       "\n",
       "   ccd_amt_kurt  ...  ccd_same_mcht_ratio  ccd_recent_7d_count  \\\n",
       "0     -0.325926  ...             0.916667                  NaN   \n",
       "1      9.000000  ...             0.888889                  NaN   \n",
       "2     -0.325928  ...             0.916667                  NaN   \n",
       "3      4.994665  ...             0.600000                  3.0   \n",
       "4     -2.224845  ...             0.900000                  1.0   \n",
       "\n",
       "   ccd_recent_7d_amt_sum  ccd_recent_7d_amt_mean  ccd_recent_14d_count  \\\n",
       "0                    NaN                     NaN                   3.0   \n",
       "1                    NaN                     NaN                   NaN   \n",
       "2                    NaN                     NaN                   1.0   \n",
       "3                4998.99                 1666.33                   3.0   \n",
       "4                   1.00                    1.00                   1.0   \n",
       "\n",
       "   ccd_recent_14d_amt_sum  ccd_recent_14d_amt_mean  ccd_recent_30d_count  \\\n",
       "0                 8748.99                  2916.33                   4.0   \n",
       "1                     NaN                      NaN                   NaN   \n",
       "2                 4500.00                  4500.00                   4.0   \n",
       "3                 4998.99                  1666.33                   5.0   \n",
       "4                    1.00                     1.00                   4.0   \n",
       "\n",
       "   ccd_recent_30d_amt_sum  ccd_recent_30d_amt_mean  \n",
       "0                 9332.32                 2333.080  \n",
       "1                     NaN                      NaN  \n",
       "2                13824.00                 3456.000  \n",
       "3                85332.32                17066.464  \n",
       "4                   44.00                   11.000  \n",
       "\n",
       "[5 rows x 132 columns]"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A_ccd_features.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "af338019",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "测试集A特征信息:\n",
      "客户数: 18\n",
      "特征数: 131\n",
      "缺失值统计:\n",
      "ccd_recent_7d_amt_sum                                     9\n",
      "ccd_recent_7d_count                                       9\n",
      "ccd_recent_7d_amt_mean                                    9\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_std_2       8\n",
      "ccd_amt_growth_m1_m2                                      8\n",
      "ccd_amt_growth_m0_m2                                      8\n",
      "ccd_sliding_CUST_NO_date_months_to_now_count_sum_2        7\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_max_2       7\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_median_2    7\n",
      "ccd_sliding_CUST_NO_date_months_to_now_TR_AMT_sum_2       7\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "print(f\"测试集A特征信息:\")\n",
    "print(f\"客户数: {A_ccd_features['CUST_NO'].nunique()}\")\n",
    "print(f\"特征数: {len(A_ccd_features.columns) - 1}\")\n",
    "print(f\"缺失值统计:\")\n",
    "print(A_ccd_features.isnull().sum().sort_values(ascending=False).head(10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0322d47",
   "metadata": {},
   "source": [
    "### 保存测试集A特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "b2557610",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "测试集A特征文件已保存: ./feature\\A_CCD_TR_DTL_features.pkl\n",
      "文件大小: 0.02 MB\n"
     ]
    }
   ],
   "source": [
    "output_file = os.path.join(feature_dir, 'A_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(A_ccd_features, f)\n",
    "\n",
    "print(f\"\\n测试集A特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0930b3a8",
   "metadata": {},
   "source": [
    "### 特征一致性检查"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0be4788",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\\n\" + \"=\"*60)\n",
    "print(\"特征一致性检查\")\n",
    "print(\"=\"*60)\n",
    "\n",
    "train_cols = set(TRAIN_ccd_features.columns)\n",
    "test_cols = set(A_ccd_features.columns)\n",
    "\n",
    "common_cols = train_cols & test_cols\n",
    "train_only = train_cols - test_cols\n",
    "test_only = test_cols - train_cols\n",
    "\n",
    "print(f\"\\n共同特征数: {len(common_cols)}\")\n",
    "print(f\"仅训练集特征数: {len(train_only)}\")\n",
    "print(f\"仅测试集特征数: {len(test_only)}\")\n",
    "\n",
    "if train_only:\n",
    "    print(f\"\\n仅训练集特征: {train_only}\")\n",
    "if test_only:\n",
    "    print(f\"\\n仅测试集特征: {test_only}\")\n",
    "\n",
    "print(\"\\n特征类型一致性检查:\")\n",
    "for col in list(common_cols)[:10]:\n",
    "    if col != 'CUST_NO':\n",
    "        train_dtype = TRAIN_ccd_features[col].dtype\n",
    "        test_dtype = A_ccd_features[col].dtype\n",
    "        if train_dtype != test_dtype:\n",
    "            print(f\"特征 {col} 类型不一致: 训练集 {train_dtype}, 测试集 {test_dtype}\")\n",
    "\n",
    "print(\"\\n特征工程完成，可用于建模训练!\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
